Add a nushell hook - #8
Open
edaywalid wants to merge 8 commits into
Open
Conversation
Ports undo.fish to nushell. Same shape: pre_execution opens the session and arms LD_PRELOAD, pre_prompt writes the done marker, restores the previous LD_PRELOAD and runs the gc. Two things differ from the other shells. Nushell has no %N in format date, so the session id comes from an int date, which is already nanoseconds. And a hook cannot set $env directly, since only what goes through load-env and hide-env reaches the caller, so the arming is written in terms of those. Not verified end to end. See the PR for what was checked and what was not.
nushell runs rm, mv, cp, mkdir and save inside its own process. There is no child for LD_PRELOAD to attach to, so the shim has to live in nu itself, which zsh already does under UNDO_CAPTURE_SHELL. The part zsh's approach cannot cover is telling the shim which session it is in: `$env.X = ...` only builds the environment handed to processes nu starts, and never touches the running nu, which is the process holding the shim. Set the session that way and the shim reads a variable nushell never set. So the nu hook exports UNDO_SESSION_PTR once, before the exec that loads the shim, and rewrites the file it names before every command. session_dir() falls back to reading it when UNDO_SESSION is absent. The cost is confined. Every other shell exports UNDO_SESSION and returns on the line above, having paid one getenv that finds nothing. Nushell pays one pread per intercepted call, from a descriptor held open on a page-cached file, which is the price of recording anything there at all. The descriptor is released by the same TLS destructor as the journal and the budget mapping, so this does not reintroduce the per-thread leak.
Driven under a pty, the hook as it stood recorded nothing at all. `rm file`, `undo`, and the file was gone: nushell's rm is a built-in, so no process is ever exec'd and the shim armed around child processes has nothing to attach to. Empty sessions, then gc removing them. The hook now re-execs nu once with the shim preloaded and hands the session over through UNDO_SESSION_PTR. Everything the shim reads is settled before that exec, because the exec is what turns $env into a real environment. Riding inside nu has two consequences worth naming: Nushell writes files for itself. Left alone every session recorded nu rewriting its own history, so `undo` after a plain `ls` offered to put an older history file back. Its config, data and cache directories join the ignore list, and so does the store, since the hooks rewrite the session pointer while the shim is still armed. `cp` cannot be caught at all. It goes straight to the kernel rather than through libc, which I confirmed by tracing every libc open during a copy and finding none, so a copy over an existing file destroyed the target and journaled nothing. `alias cp = ^cp` hands the name to coreutils, whose flags are a superset. It has to sit at the top level: an alias declared inside the `if` guard never leaves it. Also fixed from review: the done marker is written only if the session directory still exists, a degraded session is reported at the prompt, _UNDO_PREV_PRELOAD is gone rather than exported to every child, and the gc fallback no longer deletes directories from inside a `where`.
hook.sh feeds the other shells from a pipe. nushell cannot be tested that way: reedline asks the terminal for the cursor position and blocks until something answers, so nu loops on the prompt and drops every command fed to it. That is why this hook shipped unrun. pty-drive.py answers the query, which is enough to make hooks fire. The nu case asserts the coverage undo.nu depends on rather than a single smoke path, because all of it rests on nushell internals that a release could change without warning: built-in rm records an unlink, cp over an existing file records a mod, save over an existing file records a mod and undo puts it back. It also asserts nushell's own history is never journaled. Verified the assertions fail without the code that satisfies them: removing the cp alias fails the cp case.
Nothing installed undo.nu, so the file reached no one: not the Makefile, not install.sh, not the deb, rpm or Arch packages. Homebrew already globbed shell/*. install.sh prepends the source line for nushell rather than appending it. undo.nu re-execs nu, so config above that line runs a second time, and a config that prepends to PATH would do it twice. For the same reason nushell gets no PATH line of its own. The README says plainly which nushell commands are recorded, that cp is covered only through the alias, and why the line belongs at the top of config.nu.
test/hook.sh skips itself when its shell is missing, which is why the workflow already installs zsh and fish before running the suite rather than after. nushell is not in apt, so it comes from the release tarball. The version is pinned on purpose. This test asserts what nushell's built-ins do, so an unpinned upgrade would land a nushell change as a mystery failure on somebody else's PR. Bumping the pin is how we choose to find out.
edaywalid
force-pushed
the
feat/nushell-hook
branch
from
August 12, 2026 01:04
1a776aa to
61f832f
Compare
install.sh installs from the latest release, and v0.3.0 shipped before the nushell hook existed, so unconditionally installing shell/undo.nu killed the installer outright for every shell. Install it when the tarball has it. The condition stops mattering once a release carries the file, and costs nothing after that.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Ports
undo.fishto nushell, for the open request to support it.Same shape as the other hooks.
pre_executionopens the sessiondirectory, writes
cmdandpid, and puts the shim at the front ofLD_PRELOADafter dropping any otherlibundo.so.pre_promptwritesthe
donemarker, restores the previousLD_PRELOAD, and runs the gc.Two things nushell does differently:
%Ninformat date, so the session id comes fromdate now | into int, which is already nanoseconds.$envdirectly and have it stick. Onlyload-envand
hide-envreach the caller, so the arming and disarming go throughthose, and the hook bodies are blocks rather than closures because that
is the form that preserves the environment.
What I checked
UNDO_HOOK=nu.pre_executionbody builds a correct session: 16 character id,cmd,pidanddata/all present.What I could not check
The full arm and disarm cycle in a live shell. Nushell's line editor
takes input from a pty but never executes it, so I could not drive a real
session from a script, with
script(1),expect, or a pty driver. Thatalso means this hook is not in the CI smoke tests that now cover zsh,
bash and fish.
So this needs someone with nushell to confirm the part that matters:
undoshould list thermand put the file back. If it says "nothing toundo", the hook is not arming and this is not ready.
Left out of this PR on purpose: the installer, the README and the
packaging still do not mention nushell. Nothing should advertise the hook
until the above is confirmed.